package com.situ.mymall.aspects;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import com.situ.mymall.annotation.MyLog;
import com.situ.mymall.pojo.entity.Admin;
import com.situ.mymall.pojo.entity.OperLog;
import com.situ.mymall.pojo.query.AdminQuery;
import com.situ.mymall.service.IOperLogService;
import com.situ.mymall.util.PageResult;
import com.situ.mymall.util.Result;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;
import java.util.Objects;

@Log4j2
@Aspect
@Component
public class OperLogAspect {

    String LOG_INFO = "INFO";
    String LOG_DEBUG = "DEBUG";
    String LOG_ERROR = "ERROR";

    // 定义一个本地线程的对象
    private ThreadLocal<OperLog> operLogThreadLocal = new ThreadLocal<>();

    @Autowired
    private IOperLogService operLogService;
    Admin admin = null;

    /**
     * 统计日志切点
     */
    //@Pointcut("execution(* com.situ.springboot.service.impl.*.*(..))")
    @Pointcut("@annotation(com.situ.mymall.annotation.MyLog)")
    public void operLogAspect() {
    }

    /**
     * 前置通知@Before：在方法调用之前执行
     */
    @Before(value = "operLogAspect()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        /*
         *   1.Objects.requireNonNull() : 这个方法检查传入的对象是否为 null。如果是 null，它会抛出 NullPointerException。
         *   2.RequestContextHolder.getRequestAttributes() : 这个方法从当前线程中获取与请求上下文相关联的属性
         *   3..getRequest() : 用于返回当前线程绑定的 HttpServletRequest 对象
         **/

        // 定义的Log对象
        OperLog operLog = new OperLog();
        // 获取Session上已经登陆上的User
        HttpServletRequest request = ((ServletRequestAttributes) Objects
                .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        // JoinPoint 可以是在方法调用、方法执行、异常抛出或者访问属性等。
        /*
        * getArgs()：返回方法的参数。
        * getThis()：返回代理对象。
        * getTarget()：返回目标对象。
        * getSignature()：返回连接点签名（方法签名）。
        * toString()：返回连接点的字符串表示。
        * */
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取该签名的方法名称
        Method method = signature.getMethod();
        // 获取该方法的注解
        MyLog myLog = method.getAnnotation(MyLog.class);
        // 如果该注解不为空并且module不为空 就把module存储到operLog
        if (!ObjectUtils.isEmpty(myLog) && !ObjectUtils.isEmpty(myLog.module())) {
            operLog.setModule(myLog.module());
        }
        // 存储时间
        operLog.setStartTime(new Date());
        // 获取URI地址
        operLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
          // 获取参数的键值对

        operLog.setRequestParams(formatParams(request.getParameterMap()));
        // 获取请求方法
        operLog.setRequestMethod(request.getMethod());
        // 获取客户端IP地址
        operLog.setRequestIp(JakartaServletUtil.getClientIP(request));
        // 服务端地址
        operLog.setServerAddress(request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort());
        // 获取用户的设备类型
        String userAgentStr = request.getHeader("User-Agent");
        operLog.setUserAgent(userAgentStr);

        UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
        operLog.setDeviceName(userAgent.getOs().getName());
        operLog.setBrowserName(userAgent.getBrowser().getName());

        operLogThreadLocal.set(operLog);

        log.info("开始计时: {}  URI: {}  IP: {}", operLog.getStartTime(), operLog.getRequestUri(), operLog.getRequestIp());

    }


    /*
     * 最终通知 @After:在方法调用之后执行，类似于finally
     * */

    /**
     * 后置通知@AfterReturning：在方法正常调用之后执行
     */
    @AfterReturning(pointcut = "operLogAspect()", returning = "ret")
    public void doAfterReturning(Object ret) {
        OperLog operLog = operLogThreadLocal.get();
        operLog.setLogType(LOG_INFO);
        operLog.setEndTime(new Date());
        HttpServletRequest request = ((ServletRequestAttributes) Objects
                .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        HttpSession session = request.getSession();
        admin = (Admin) session.getAttribute("admin");
        // 如果获取到的admin不为null就把id和name存储到Log对象中
        if (admin != null) {
            operLog.setUserId(admin.getId());
            operLog.setUserName(admin.getName());
        }
        operLog.setExecuteTime(Long.valueOf(ChronoUnit.MILLIS.between(LocalDateTime.ofInstant(operLog.getStartTime().toInstant(), ZoneId.systemDefault()),
                LocalDateTime.ofInstant(operLog.getEndTime().toInstant(), ZoneId.systemDefault()))));
        operLog.setIsException((byte) 0);
        // 如果是Result
        if (ret instanceof Result) {
            Result r = Convert.convert(Result.class, ret);
            try {
                operLog.setReponseParams(new ObjectMapper().writeValueAsString(r));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        // 如果是PageResult的话
        } else if (ret instanceof PageResult) {
            PageResult r = Convert.convert(PageResult.class, ret);
            try {
                operLog.setReponseParams(new ObjectMapper().writeValueAsString(r));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        // 如果是String类型的话
        } else if (ret instanceof String) {//转发界面
            operLog.setReponseParams((String) ret);
        }
        operLogService.add(operLog);
        operLogThreadLocal.remove();

        Runtime runtime = Runtime.getRuntime();
        log.info("计时结束: {}  用时: {}ms  URI: {} ", operLog.getEndTime(), operLog.getExecuteTime(),
                operLog.getRequestUri());
    }

    /**
     * 异常通知@AfterThrowing:如果在方法调用过程中发生异常，则通知
     */
    @AfterThrowing(pointcut = "operLogAspect()", throwing = "e")
    public void doAfterThrowable(Throwable e) {
        OperLog operLog = operLogThreadLocal.get();
        operLog.setLogType(LOG_ERROR);
        operLog.setEndTime(new Date());
        //operLog.setExecuteTime(Long.valueOf(ChronoUnit.MINUTES.between(operLog.getStartTime(), operLog.getEndTime())));
        operLog.setExecuteTime(Long.valueOf(ChronoUnit.MILLIS.between(LocalDateTime.ofInstant(operLog.getStartTime().toInstant(), ZoneId.systemDefault()),
                LocalDateTime.ofInstant(operLog.getEndTime().toInstant(), ZoneId.systemDefault()))));
        operLog.setIsException((byte) 1);
        operLog.setExceptionMsg(e.getMessage());
        operLog.setIsDeleted((byte) 0);
        operLogService.add(operLog);
        operLogThreadLocal.remove();

        log.info("计时结束: {}  用时: {}ms  URI: {} ", operLog.getEndTime(), operLog.getExecuteTime(),
                operLog.getRequestUri());
    }

    /**
     * 格式化参数
     */
    private String formatParams(Map<String, String[]> parameterMap) {
        if (parameterMap == null) {
            return null;
        }
        StringBuilder params = new StringBuilder();
        for (Map.Entry<String, String[]> param : (parameterMap).entrySet()) {
            if (!params.isEmpty()) {
                params.append("&");
            }
            params.append(param.getKey()).append("=");
            if (StrUtil.endWithIgnoreCase(param.getKey(), "password")) {
                params.append("*");
            } else if (param.getValue() != null) {
                params.append(ArrayUtil.join(param.getValue(), ","));
            }
        }
        return params.toString();
    }


}
